#!/usr/bin/ruby
#
# Copyright:: Copyright 2012, Trimble Navigation Limited
# License:: All Rights Reserved.
# Original Author:: Scott Lininger (mailto:scott@sketchup.com)
#
# Declares a tool in SU that allows users to "interact" with dynamic components,
# by doing things like clicking to start an animation or other interaction.

class DCInteractTool

  # Initialize the DCInteractTool object. Creates some cursor instance variables
  # that are used to highlight
  #
  #  Args:
  #     None.
  def initialize(observers_instance)
    @dcobservers = observers_instance

    @cursor_default = UI.create_cursor(
      DynamicComponents::Util::get_image_file("cursor_interact_tool"), 3, 2
    )
    @cursor_active = UI.create_cursor(
      DynamicComponents::Util::get_image_file("cursor_interact_tool_active"), 9, 9
    )
    @cursor_noactions = UI.create_cursor(
      DynamicComponents::Util::get_image_file("cursor_interact_tool_noactions"), 9, 9
    )

    # Keep track of whether we are over an entity to set our cursor nicely.
    @is_over_entity = false

    @animation_list = []

    @hover_message = "Hover over a component to find its click behaviors."
    @hover_message = @dcobservers.translate(@hover_message)

    # Create a boolean to record if we're curently animating
    @is_animating = false

    # This is our converter tool kit. We will use this to get at a list of
    # reserved attributes.
    @conv = DCConverter.new
  end

  # Called when the user first activates the tool.
  #
  #  Args:
  #     None.
  def activate
    Sketchup::set_status_text @dcobservers.translate(@hover_message)
  end

  # Called when the user deactivates the tool.
  #
  #  Args:
  #     view    the active view
  def deactivate(view)
    @animation_list = []
    stop_last_timer()
  end

  # Called by SketchUp to determine how large of a bounding box to draw within.
  # This method simply takes the model's bounding box and multiplies it in
  # size, so we're likely to capture animations that go beyond the current
  # model bounds.
  #
  #  Args:
  #     None.
  def getExtents
    if @animation_list.length > 0
      new_bounds = Geom::BoundingBox.new
      for animation in @animation_list
        entity = animation["entity"]
        is_camera = entity.to_s.include? "Sketchup::Camera"
        if is_camera == false
          new_bounds.add entity.bounds
        end
      end
      return new_bounds
    end
  end

  # Called by SketchUp when the cursor changes.
  #
  #  Args:
  #     None.
  def onSetCursor()
    if @onclick_action.to_s != ""
      UI.set_cursor(@cursor_active)
    elsif @is_over_entity == true
      UI.set_cursor(@cursor_noactions)
    else
      UI.set_cursor(@cursor_default)
    end
  end

  # As the mouse moves about, we create an input point and attempt to store
  # any entity that we're over which contains onclick attributes. If we find
  # one, we place it in @onclick_entity so that the onClick can pull that
  # entity and respond accordingly.
  #
  #  Args:
  #     flags, x, y, view
  def onMouseMove(flags, x, y, view)

    # There is a strange timing bug on the mac where the animation timer is
    # conflicting with our @timer handle, so by bailing out from the callback
    # if the camera is animating, we keep the animating running until the end.
    if camera_is_animating()
      return
    end

    # We used to stop the timer inside the timer event itself, but that was
    # causing crashes on the mac. So we now check for timers to clear out
    # here.
    stop_last_timer()

    @onclick_action = ""
    @onclick_entity = nil
    @dc = nil

    ph = view.pick_helper
    ph.do_pick x, y
    pick_list = ph.path_at(0)

    # Assume we're not hovering over anything. We'll change this flag if we
    # find otherwise.
    @is_over_entity = false

    if (pick_list != nil)
      for i in 0..pick_list.length - 1
        entity = pick_list[i]
        @is_over_entity = true
        if entity.typename == "ComponentInstance" || entity.typename == "Group"
          @dc = @dcobservers.get_class_by_version(entity)
          @onclick_action = @dc.get_attribute_value(entity, "onclick")
          if (@onclick_action != nil)
            @onclick_entity = entity

            msg = @dcobservers.translate(
              @dc.get_attribute_formlabel(entity, "onclick")
            )
            msg = @dc.second_if_empty(msg,
                                      @dcobservers.translate("Click to activate."))

            Sketchup::set_status_text msg
            Sketchup.active_model.active_view.tooltip = "   " +
                                                        @dcobservers.translate(msg)
            break
          else
            msg = @dcobservers.translate("No click behaviors.")
            Sketchup::set_status_text msg
            Sketchup.active_model.active_view.tooltip = ""
          end
        end
      end
    else
      Sketchup::set_status_text @dcobservers.translate(@hover_message)
    end
  end

  # When the user clicks, we simply look at @onclick_action that was previously
  # set in onMouseMove to determine what to do.
  #
  #  Args:
  #     flags, x, y, view
  def onLButtonUp(flags, x, y, view)

    # There is a strange timing bug on the mac where the animation timer is
    # conflicting with our @timer handle, so by bailing out from the callback
    # if the camera is animating, we keep the animating running until the end.
    if camera_is_animating()
      return
    end

    # If there are no actions here to look at, then bail out and avoid
    # potential errors from the has_version_error check.
    @onclick_action = @onclick_action.to_s
    if @onclick_action == ""
      return
    end

    # If our target entity is a newer version DC than our installed plugin can
    # handle, then show an error message and ignore the click.
    entity = @onclick_entity
    if @dcobservers.has_version_error(entity)
      return
    end

    # If we're about to begin one or more animations, start an operation
    # to wrap them all at once. (Provided we're not already animating.)
    if @onclick_action.downcase.include?("animate") or
       @onclick_action.downcase.include?("set(") &&
       @is_animating == false
      Sketchup.active_model.start_operation @dc.translate("Interact"), true
      @is_animating = true
    end

    # Escape any characters that are inside double quotes.
    escaped_action = @onclick_action.gsub(/\"([^\"]+)?\"/) { |match|
      quoted_string = @dc.second_if_empty($1, "")
      '"' + @dc.escape(quoted_string, false) + '"'
    }

    # Split our onClick commands at semicolons, javascript-style. Then parse
    # each command in turn.
    commands = escaped_action.split(";")

    # Create a place to store the "number" of the command that we're parsing.
    command_count = 0

    for command in commands

      # If a command is empty or the command contains no word characters, then
      # skip it. This command is just empty white space.
      if command.to_s == ""
        next
      elsif command.index(/\w/) == nil
        next
      end

      command_count = command_count + 1

      # Figure out what the function and parameters are.
      first_parens = command.index("(")
      last_parens = command.rindex(")")

      # Show an error if parenthesis are not present.
      if first_parens == nil || last_parens == nil
        UI.messagebox(@dc.translate("ERROR: Unmatched parenthesis in ") +
                      command)
        return
      end

      function = command[0..first_parens - 1].strip.downcase
      param_string = command[first_parens + 1..last_parens - 1]

      # Escape any commas that are inside nested function calls.
      param_string = escape_commas_in_parens(param_string)

      # Split on remaining commas, and then unescape any nested
      # commas or other escaped characters inside quotes. Note that
      # url escaping turns '+' into a space, so replace any actual
      # plus symbols with %2B.
      params = param_string.split(",")
      for i in 0..params.length - 1
        params[i] = params[i].gsub(/\+/, "%2B")
        params[i] = @dc.unescape(params[i])
      end

      # Handle our list of valid functions.
      if function == "alert"
        message, error = @dc.parse_formula(params[0], entity)
        if error.to_s.include? "subformula-error"
          UI.messagebox(@dc.translate("ERROR IN FORMULA: ") +
                        @dc.unescape(param_string))
        else
          # Unescape common string markups for the alert message.
          message = message.to_s
          message = message.gsub(/\\n/, "\n")
          message = message.gsub(/\\r/, "\r")
          message = message.gsub(/\\t/, "\t")
          UI.messagebox(message)
        end
      elsif function == "redraw"
        @dc.redraw_with_undo(entity)
      elsif function == "gotoscene"
        scene_name = params.shift
        scene_name, error = @dc.parse_formula(scene_name, entity)

        length = @dc.second_if_empty(params.shift, 3.0)
        easein = @dc.second_if_empty(params.shift, 100)
        easeout = @dc.second_if_empty(params.shift, 100)

        length, error = @dc.parse_formula(length, entity)
        easein, error = @dc.parse_formula(easein, entity)
        easeout, error = @dc.parse_formula(easeout, entity)

        length = length.to_f

        # The passed easein and easeout are between 0 and 100, but the
        # math is simpler if it's between 0.0 and 1.0, so adjust for that
        easein = easein.to_f / 100.0
        easeout = easeout.to_f / 100.0

        # Find which page (name or number) that we want to go to
        model = Sketchup.active_model
        for page in model.pages
          if page.name.downcase == scene_name.to_s.downcase
            target_page = page
          end
        end
        if target_page == nil && scene_name.to_i > 0 &&
           scene_name.to_i <= model.pages.count
          target_page = model.pages[scene_name.to_i - 1]
        end

        # If we've found a valid page and we're not already there, begin a
        # new animation.
        if target_page != nil
          current_camera = model.active_view.camera
          target_camera = target_page.camera
          if current_camera.eye != target_camera.eye ||
             current_camera.target != target_camera.target ||
             current_camera.up != target_camera.up
            attach_animation(current_camera, "", target_camera, length, easein, easeout)
          end
        else
          UI.messagebox(@dc.translate("ERROR: Scene could not be found: ") +
                        scene_name)
        end
      elsif function.index("animate") == 0 or function == "set"

        # Make the thing we're animating unique.
        @dc.make_unique_if_needed(entity)

        # "Set" is an alias for animateInstant. Combining the set and animate
        # functionality into one code block simplifies everything, since a set
        # is really nothing more that an instant animation.
        if function == "set"
          function = "animateinstant"
        end

        # There are several animate functions that are valid:
        #
        # animate(reference,  value_1,value_2,...value_n)
        # animateSlow(reference,  value_1,value_2,...value_n)
        # animateFast(reference,  value_1,value_2,...value_n)
        # animateCustom(reference,length,easein,easeout,  value_1,...value_n)
        #
        # So handle each of them here by parsing our the "type" string that
        # appears after the word "animate"
        animate_type = function[7..999]
        reference_string = params.shift
        reference, error = @dc.parse_formula(reference_string, entity)

        # There was a bug (1499824) where an incorrectly formatted reference
        # such as '"rotx"0' was naturally returning a formula error, but
        # we were not capturing the error, leading to corrupted DCs with
        # attribute names that were empty strings. This block fixes this by
        # ensuring that we have a valid string to work with. - scottlininger
        if error.to_s.include? "subformula-error" || reference == nil ||
                                 reference == ""
          UI.messagebox(@dc.translate("ERROR: Invalid entity to animate: ") +
                        " (" + reference_string + ")")
          return
        end

        # If the user entered a 1st parameter like animate(rotx,0,90), then
        # we can reasonably assume that what they meant was
        # animate("rotx",0,90). There are two ways that we check to detect if
        # this is the case. First, if the naked reference string contains the
        # name of a reserved attribute that is a STRING type. Second, we can
        # look to see if the returned result of the formula is empty or a
        # number... if so, then let's assume the original
        # reference string was the actual reference they wanted.
        if @conv.reserved_attribute_group[reference_string.downcase] == "STRING"
          reference = reference_string.downcase
        elsif reference == nil || reference == "" ||
              reference.to_s =~ /^\d+\.*\d*$/
          reference = reference_string.downcase
        end

        # If our reference is still nil, then return.
        if reference == nil || reference == ""
          return
        end

        if animate_type == "custom"
          length = params.shift
          easein = params.shift
          easeout = params.shift

          length, error = @dc.parse_formula(length, entity)
          easein, error = @dc.parse_formula(easein, entity)
          easeout, error = @dc.parse_formula(easeout, entity)

          length = length.to_f

          # The passed easein and easeout are between 0 and 100, but the
          # math is simpler if it's between 0.0 and 1.0, so adjust for that
          easein = easein.to_f / 100.0
          easeout = easeout.to_f / 100.0
        elsif animate_type == "slow"
          length = 1.0
          easein = 0.0
          easeout = 1.0
        elsif animate_type == "fast"
          length = 0.25
          easein = 0.0
          easeout = 1.0
        elsif animate_type == "instant"
          length = 0.0
          easein = 0.0
          easeout = 0.0
        else
          length = 0.5
          easein = 0.0
          easeout = 1.0
        end

        # Now that we've parsed our variations in the animate functions and
        # their parameter lists, anything that remains on the params array
        # is part of our list of values to animate between.
        value_list = params
        if value_list.length < 1
          return
        end

        value_list = parse_as_formulas(value_list, entity, @dc, reference)

        entity, attribute = parse_command_reference(reference)
        if entity == nil
          UI.messagebox(@dc.translate("ERROR: Invalid entity to animate: ") +
                        " (" + reference + ")")
          return
        else
          @dc.make_unique_if_needed(entity)
        end

        current_value = @dc.get_attribute_value(entity, attribute)

        # See if we explicitly set the last animation state as an attribute. If
        # so, we will use that to determine the next in line.
        last_list_index = @dc.get_attribute_value(entity,
                                                  "_onclick_state" + command_count.to_s)

        next_value, next_index = next_value_from(value_list, current_value,
                                                 last_list_index)

        # Now attach the animation and go to town
        if is_already_animating(entity, attribute) == false
          attach_animation(entity, attribute, next_value, length, easein,
                           easeout)
        end

        # Store the state of the animation that we've started moving towards
        # so that future onlick actions have some extra context to work from.
        @dc.set_attribute(entity, "_onclick_state" + command_count.to_s,
                          next_index)
      else
        UI.messagebox(@dc.translate("ERROR: Unknown function:") + " (" +
                      command + ")")
      end
    end

    # Force an active cursor to avoid cursor flashing on the PC.
    UI.set_cursor(@cursor_default)
  end

  ##############################################################################
  # Support Methods
  ##############################################################################

  # Clear out our last timer.
  def stop_last_timer
    if @animation_list.length == 0 && @timer != nil
      UI.stop_timer @timer
      @timer = nil
    end
  end

  # Returns whether a given animation is alredy happening.
  def is_already_animating(entity, attribute)
    for animation in @animation_list
      if animation["entity"] == entity &&
         animation["attribute"] == attribute
        return true
      end
    end
    return false
  end

  # Parse a series of values as formulas, convert them to the appropriate unit
  # type, and return.
  #  Args:
  #     params: array of strings containing the formulas to parse
  #     entity: the entity in whose context the formulas must run
  #         dc: the dc object to use for the parsing
  #  attribute: the attribute to whose unit we should convert the results
  def parse_as_formulas(params, entity, dc, attribute)
    for i in 0..params.length - 1
      formula = params[i]
      # Parse and convert the list of values into the base unit, guided by
      # the formula unit on the target attribute. (For example: if we're
      # animating an attribute between "1" and "2" but that attribute is
      # in centimeters, then really we're animating between 2.54 and 5.08.
      params[i], error = dc.parse_formula(formula, entity, attribute)
      if error.to_s.include? "subformula-error"
        UI.messagebox(@dc.translate("ERROR: could not parse formula: ") +
                      formula.to_s)
      end
    end
    return params
  end

  # Figure out where in a list we currently are, and grab the next
  # value from the list.
  def next_value_from(params, current_value, current_index = 0)
    # If we have no current state, then default to our first animation state.
    if current_value == nil || current_value == ""
      return params[0], 0
    end

    # Create some places to store results as we find them.
    current_index = current_index.to_i
    found_index = nil
    last_matched_index = nil

    # Look for string matches first...
    for i in 0..params.length - 1
      value = params[i]
      if current_value.to_s == value.to_s
        if i < current_index
          last_matched_index = i
        else
          found_index = i
          break
        end
      end
    end

    # Look for Float matches if we haven't found a match yet.
    if found_index == nil
      for i in 0..params.length - 1
        value = params[i]
        if (current_value.to_f - value.to_f).abs < 0.001
          if i < current_index
            last_matched_index = i
          else
            found_index = i
            break
          end
        end
      end
    end

    # If we didn't find a definite match that was *after* the state
    # that we started with, then we'll fall back to any match
    # that was made.
    if found_index == nil
      found_index = last_matched_index
    end

    if found_index == nil
      next_index = 0
    else
      next_index = found_index + 1
      if next_index == params.length
        next_index = 0
      end
    end
    next_value = params[next_index]
    return next_value, next_index
  end

  # Attempts to take an attribute reference, such as "Cabinet!lenx" and return
  # an actual entity that is refers to as well as the attribute name.
  #
  #  Args:
  #     reference(string)  name of the reference we're trying to parse
  def parse_command_reference(reference)
    source_entity = @onclick_entity
    if reference.to_s.index("!")
      sheet_name = reference[0..reference.index("!") - 1]
      attribute_name = reference[reference.index("!") + 1..999]
      entity = @dc.find_entity_by_sheet_name(source_entity, sheet_name)
      return entity, attribute_name
    else
      return source_entity, reference
    end
  end

  # Attaches a new animation based on a series of parameters
  #
  # Params:
  #     entity: entity that we're animating
  #     attribute: the attribute that's changing over time
  #     targetval: what the attribute will be at the end of the animation
  #     length: length of the animation, in seconds
  #     easein: number between 0 and 1.0 defining how much easing in to do
  #     easeout: number between 0 and 1.0 defining how much easing out to do
  #  Args:
  #     reference(string)  name of the reference we're trying to parse
  def attach_animation(entity, attribute, targetval, length, easein, easeout)

    # Make sure that our easing is within bounds (0-1)
    if easein > 1.0
      easein = 1.0
    elsif easein < 0.0
      easein = 0.0
    end

    if easein > 1.0
      easein = 1.0
    elsif easein < 0.0
      easein = 0.0
    end

    # Create an hash that contains everything about the animation, including
    # the appropriate version of the DynamicComponentsClass (dc) to run the
    # attribute setting/getting and redrawing through.
    animation = {}
    animation["dc"] = @dc
    animation["entity"] = entity
    animation["attribute"] = attribute
    animation["start_time"] = Time.new
    animation["length"] = length.to_f

    is_camera = entity.to_s.include? "Sketchup::Camera"
    if is_camera
      animation["startval"] = Sketchup::Camera.new entity.eye, entity.target, entity.up
      animation["targetval"] = targetval
    else
      animation["startval"] = @dc.get_attribute_value(entity, attribute)

      # If the targetval is a number, then store target as a float. Otherwise
      # store as a string.
      if targetval.to_f.to_s == targetval.to_s ||
         targetval.to_i.to_s == targetval.to_s
        animation["targetval"] = targetval.to_f
      else
        animation["targetval"] = targetval.to_s
      end
    end

    # Calculate our easing segments. Every animation consists of an
    # easein segment, followed by a center segment, followed by an
    # easeout. The relative sizes of each of these segments is weighted
    # by the overall size of our easein and easout parameters.
    center_weight = (1.0 - easein) * easein + (1.0 - easeout) * easeout
    total_weight = center_weight + easein + easeout
    if total_weight != 0.0
      easein_end = easein / total_weight
      easeout_start = 1.0 - (easeout / total_weight)
    else
      easein_end = 0.0
      easeout_start = 1.0
    end

    animation["easein_end"] = easein_end
    animation["easeout_start"] = easeout_start

    # Push our animation onto the list of all animations, and start an operation
    # that will ultimately be closed once all animations complete.
    @animation_list.push animation

    # Clear the progress bar, to ensure we don't have errors if the user is
    # clicking on an animation before doing any other progress bar interactions.
    DCProgressBar.clear()

    # If the timer isn't already going, create a new one.
    if @timer == nil

      # TODO(scottlininger): Do we want the auto-calculate the framerate?
      # Perhaps based on the users's machine speed? Or make it configurable?
      @timer = UI.start_timer(1.0 / 12.0, true) {

        # There is a crash on the mac if you close a model where an active
        # timer is running, so this method registers our timer with the
        # observers object so we can clear it out if the model is deleted.
        @dcobservers.register_timer_id(@timer)

        if @is_in_timer_handler == true

          # Then do nothing in this frame.

        else

          # Note the fact that we're processing a frame so that we don't have
          # timer callback collisions.
          @is_in_timer_handler = true

          # Calculate how much to advance the animation based on the amount
          # of time that has passed. At the end of the day, we'll get a
          # fraction_complete between 0.0 and 1.0
          current_time = Time.new

          # Loop across any animations in the list and activate the next frame
          for animation in @animation_list
            dc = animation["dc"]
            entity = animation["entity"]
            attribute = animation["attribute"]
            startval = animation["startval"]
            targetval = animation["targetval"]
            start_time = animation["start_time"]
            length = animation["length"]
            easein_end = animation["easein_end"]
            easeout_start = animation["easeout_start"]

            is_camera = entity.to_s.include? "Sketchup::Camera"
            seconds_complete = (current_time - start_time).to_f
            fraction_complete = seconds_complete / length

            # If the entity we're animating has been deleted, close this
            # animation.
            if is_camera == false
              if entity.valid? == false
                @animation_list.delete(animation)
                UI.stop_timer @timer
                next
              end
            end

            # If we're at the end of the animation, set the attribute to the
            # exact target value and delete the animation from our list.
            if fraction_complete >= 1.0
              newval = targetval
              @animation_list.delete(animation)
              if is_camera
                Sketchup.active_model.active_view.camera = newval
              end
            else

              # If we're in a part of the animation that's before our easein
              # end point or after our easeout start point, adjust the fraction
              # complete to match.
              if fraction_complete < easein_end
                fraction_eased = fraction_complete / easein_end
                fraction_complete = fraction_complete *
                                    (1.0 - (Math.sin((Math::PI / 2) * (1.0 - fraction_eased))))
              elsif fraction_complete > easeout_start
                fraction_eased = (fraction_complete - easeout_start) /
                                 (1.0 - easeout_start)
                fraction_complete = fraction_complete +
                                    (1.0 - fraction_complete) *
                                    Math.sin((Math::PI / 2) * fraction_eased)
              end

              # Here's where we calculate the actual value to insert. If the
              # target value is a camera object, then move toward that camera's
              # eye. If the target value is a float, then calculate the
              # intermediate value and move to that. There is a third
              # case where the target value is a string... in such a case we
              # do not animate intermediate values as there's no easy way
              # to say what 50% of a string like "GreenColor" is, so just
              # show the start value until the animation is complete.
              if is_camera
                neweye = Geom::Point3d.linear_combination 1.0 - fraction_complete,
                                                          startval.eye, fraction_complete, targetval.eye
                newtarget = Geom::Point3d.linear_combination 1.0 -
                                                               fraction_complete, startval.target, fraction_complete,
                                                             targetval.target
                newup = Geom::Vector3d.linear_combination 1.0 - fraction_complete,
                                                          startval.up, fraction_complete, targetval.up
                Sketchup.active_model.active_view.camera.set neweye,
                  newtarget, newup
              elsif targetval.kind_of? Float
                newval = startval.to_f + ((targetval.to_f - startval.to_f) *
                                          fraction_complete)
              else
                newval = startval
              end
            end

            if is_camera == false
              # Set the attribute and redraw the component. (The false parameter on
              # redraw() tells the method not to display a progress bar.) Note that
              # we're setting both the value and the formula, to make "live value"
              # attributes animate happily.
              if newval.kind_of? String
                formula = '"' + newval + '"'
              else
                # For numeric values, we must create a formula that is in the
                # native formula units of the attribute we're targeting.
                # A formula of "=100" has different meanings depending on
                # the unit that underlies it.
                formula = @conv.from_base(newval,
                                          dc.get_attribute_formulaunits(entity, attribute, true)).to_s
                formula = formula.to_s
              end
              dc.set_attribute(entity, attribute, newval, formula)
              dc.redraw(entity, false)

              # If we're at the end of an animation, clear out any formula that
              # we've set.
              if fraction_complete >= 1.0
                # Also, since an animation could have changed the bounding box
                # sizes of the parents, update those now.
                dc.update_last_sizes(entity)
                dc.set_attribute_formula(entity, attribute, nil)
                dc.clear_instance_cache(entity, false)
              end
            end
          end

          # Invalidate the view so it actually redraws.
          Sketchup.active_model.active_view.invalidate

          # If the animation list is empty, then stop our timer and commit the
          # animation as an undo state.
          if @animation_list.length == 0 && @is_animating == true
            @is_animating = false
            Sketchup.active_model.commit_operation
            if dc != nil
              dc.refresh_dialogs()
              if dc.is_mac == false
                stop_last_timer()
              end
            end
          end

          # Clear our "in timer" note so the next frame can process.
          @is_in_timer_handler = false
        end
      }
    end
  end

  # Replaces any commas that are inside parenthesis with '%2C'. This is useful
  # for parsing onclick actions such as alert(if(myVar=true,"Yes","No")).
  #
  # Params:
  #     formula: string to search for commas
  #
  # Returns:
  #     escaped_formula: escaped version of formula
  def escape_commas_in_parens(formula)
    opening_parens_count = 0
    escaped_formula = ""
    for i in 0..(formula.length - 1)

      # Grab a character off the string and add it to the new formula. If it's
      # a comma and we're inside some parens, then escape it.
      char = formula[i..i]
      if opening_parens_count > 0 && char == ","
        escaped_formula = escaped_formula + "%2C"
      else
        escaped_formula = escaped_formula + char
      end

      # Keep track of how many parens we're nested within.
      if char == "("
        opening_parens_count = opening_parens_count + 1
      elsif char == ")"
        opening_parens_count = opening_parens_count - 1
      end
    end
    return escaped_formula
  end

  # Returns whether the camera is being animated.
  #
  # Params:
  #     None
  #
  # Returns:
  #     boolean: true if camera is persent in animation list.
  def camera_is_animating
    if @animation_list.length > 0
      for animation in @animation_list
        if animation["entity"].to_s.include? "Sketchup::Camera"
          @onclick_action = ""
          @is_over_entity = false
          return true
        end
      end
    end
    return false
  end
end
